/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package TimeRecording.reporting;

import TimeRecording.business.AccruedTime;
import TimeRecording.business.DaisyChainUtilities;
import TimeRecording.business.DateRange;
import TimeRecording.tables.AllocationOfHours;
import TimeRecording.tables.Employee;
import TimeRecording.tables.WeeklyTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.engine.util.JRLoader;
import net.sf.jasperreports.view.JasperViewer;

/**
 *
 * @author david
 */
public class CollationReport {

    private final static String COLLATION_REPORT =
            "/reports/collatedtime/TimesheetMonthReport.jasper";

    private final static Logger LOGGER =
            Logger.getLogger(CollationReport.class.getName());

    private final static int PAGE_HEIGHT = 595;
    private final static int TOP_MARGIN = 20;
    private final static int BOTTOM_MARGIN = 20;
    private final static int HEADER_HEIGHT = 32;
    private final static int FOOTER_HEIGHT = 28;

    private final static int USABLE_PAGE_HEIGHT =
            PAGE_HEIGHT - TOP_MARGIN - BOTTOM_MARGIN -
            HEADER_HEIGHT - FOOTER_HEIGHT;

    private final static int SUMMARY_HEIGHT = 64;
    private final static int FIXED_HEIGHT = 30;
    private final static int SEMI_FLEXI_HEIGHT = 30;
    private final static int FLEXI_HEIGHT = 40;
    private final static int SESSIONAL_HEIGHT = 20;
    private final static int AD_HOC_HEIGHT = 20;
    private final static int SPACER_HEIGHT = 8;

    /**
     * Displays a single week absence report containing a row for each of the
     * supplied employees.  The title of the report reflects the date range.
     *
     * @param range date range for the calendar week
     * @param iconProvider the icon provider (maps absences to icons)
     * @param entityManager the connection through which to query the database
     */
    @SuppressWarnings("unchecked")
    public void periodReport(DateRange period, EntityManager entityManager,
            boolean sessional, boolean salaried) {

        List<Employee> employees = Employee.getActiveEmployees(entityManager);
        periodReport(period, employees, entityManager, sessional, salaried);
    }

    /**
     * Displays a single week absence report for all non-deleted employees.  The
     * The title of the report reflects the date range.
     *
     * @param range date range for the calendar week
     * @param iconProvider the icon provider (maps absences to icons)
     * @param employees the employees to interrogate
     */
    @SuppressWarnings("unchecked")
    public void periodReport(DateRange period, Collection<Employee> employees,
            EntityManager entityManager, boolean sessional, boolean salaried) {

        Map<String, Object> parameters =
                new ReportingUtilities().getBasicParameters();

        String title = "Time Recorded Report for period " +
                DaisyChainUtilities.dateAsString(period.getFromDate()) +
                " to " +
                DaisyChainUtilities.dateAsString(period.getToDate());

        parameters.put("TITLE", title);

        ArrayList<TimesheetMonth> monthReports = new ArrayList<TimesheetMonth>();

        // We need to control page breaks ourselves so work out
        // how much of the page is available for printing

        int remainingHeight = USABLE_PAGE_HEIGHT;

        Query query = entityManager.createNamedQuery(
                WeeklyTime.BY_EMPLOYEE_AND_DATE_RANGE);

        for (Employee employee : employees) {

            // We'll need at least a summary
            int requiredHeight = SUMMARY_HEIGHT;

            TimesheetMonth reportMonth = new TimesheetMonth();
            reportMonth.setTitle(employee.getFriendlyName());

            GregorianCalendar todaysCalendar = new GregorianCalendar();
            DaisyChainUtilities.cleanCalendar(todaysCalendar);

            GregorianCalendar startCalendar = new GregorianCalendar();
            startCalendar.setTime(period.getFromDate());

            GregorianCalendar endCalendar = new GregorianCalendar();
            endCalendar.setTime(period.getToDate());

            // Find timesheets for the employee over the period
            query.setParameter("employeeId", employee.getEmployeeId());
            query.setParameter("startDate", DaisyChainUtilities.
                    firstDayOfWeek(startCalendar).getTime());
            query.setParameter("endDate", DaisyChainUtilities.
                    firstDayOfWeek(endCalendar).getTime());

            Collection<WeeklyTime> weeksForPeriod = query.getResultList();

            // Get our brought forward figures
            AllocationOfHours broughForwardFigures = AccruedTime.accruedToDate(
                    entityManager, period.getFromDate(), employee);

            // Use these as the basis for our running totals
            double totalFixedShortfall =
                    broughForwardFigures.getFixedShortfall();
            double totalSemiFlexiOutstanding =
                    broughForwardFigures.getSemiFlexiShortfall();
            double totalFlexiCredit =
                    broughForwardFigures.getFlexiCredit();

            AllocationOfHours totalFigures = new AllocationOfHours();

            ArrayList<TimesheetDay> days = new ArrayList<TimesheetDay>();

            while (!startCalendar.after(endCalendar)) {

                // Find the weekly time record for the chosen date
                WeeklyTime chosenWeek = null;
                for (WeeklyTime week : weeksForPeriod) {
                    if (week.inRange(startCalendar)) {
                        chosenWeek = week;
                        break;
                    }
                }

                // Create report details for the single day
                TimesheetDay day = new TimesheetDay();
                Date date = startCalendar.getTime();
                day.setDate(date);

                // If we've no timesheet then just move on.  This should never
                // really occur as we'll want to maintain empty timesheets but
                // if an employee is deleted then this is the only sensible
                // course to follow.

                if (chosenWeek == null) {
                    days.add(day);
                    startCalendar.add(GregorianCalendar.DATE, 1);
                    continue;
                }

                AllocationOfHours dailyFigures =
                        chosenWeek.getDailyFigures(startCalendar);
                day.setBase(dailyFigures);

                totalFigures.add(dailyFigures);

                int dayOfWeek = startCalendar.get(GregorianCalendar.DAY_OF_WEEK);
                boolean sunday = (dayOfWeek == GregorianCalendar.SUNDAY);

                boolean workingTimeExpectedDay = dailyFigures.getExpected() != null;

                // TODO - Not sure about this logic at all
                boolean missingTimesheetEntry =
                        workingTimeExpectedDay &&
                        chosenWeek.getClocked(startCalendar) == null &&
                        chosenWeek.getSickness(startCalendar) != null;

                //////////////////////////////////////////////////////////
                // Fill in each of the bands straight from the database //
                //////////////////////////////////////////////////////////

                ///////////////////////////
                // The "fixed" time band //
                ///////////////////////////

                // Are we expecting any time for the day?
                if (dailyFigures.getFixedExpected() != 0.0) {

                    // Only display the fixed band if salaried
                    // time is being included in the report
                    if (salaried) {
                        reportMonth.setDisplayFixed(true);
                    }

                    day.setFixedShortfall(day.getBase().getFixedShortfall());
                    totalFixedShortfall += day.getBase().getFixedShortfall();
                }

                ////////////////////////////////
                // The "semi-flexi" time band //
                ////////////////////////////////

                // Are we expecting any time for the day?

                if (dailyFigures.getSemiFlexiExpected() != 0.0 ||
                        dailyFigures.getSemiFlexiRecorded() != 0.0) {

                    // Only display the semi-flexi band if salaried
                    // time is being included in the report
                    if (salaried) {
                        reportMonth.setDisplaySemiFlexi(true);
                    }

                    // Get the cumulative figure for the ENTIRE WEEK
                    Double cumulativeExpected = chosenWeek.
                            getWeeklyFigures(null).getSemiFlexiExpected();

                    // Get the cumulative figure for the week up to
                    Double cumulativeRecorded = chosenWeek.
                            getWeeklyFigures(dayOfWeek).getSemiFlexiRecorded();

                    double semiFlexiOutstanding =
                            (cumulativeRecorded != null ? cumulativeRecorded : 0.0) -
                            (cumulativeExpected != null ? cumulativeExpected : 0.0);

                    day.setSemiFlexiOutstanding(semiFlexiOutstanding);

                    // Note that we're only updating the summary at the end of
                    // the week so as to allow the opportunity to make up the
                    // hours.  There's an assumption that this report will be
                    // run on a weekday.  If someone's hours do fall short (as
                    // calculated in the following month) then they'll take the
                    // hit then).

                    if (sunday) {
                        totalSemiFlexiOutstanding += semiFlexiOutstanding;
                    }
                }

                ////////////////////////
                // The flexi timeband //
                ////////////////////////

                if (dailyFigures.getFlexiExpected() != 0.0 ||
                        dailyFigures.getFlexiRecorded() != 0.0) {

                    // Only display the flexi band if salaried
                    // time is being included in the report
                    if (salaried) {
                        reportMonth.setDisplayFlexi(true);
                    }

                    double flexiCredit = dailyFigures.getFlexiCredit();
                    totalFlexiCredit += flexiCredit;

                    day.setFlexiCredit(totalFlexiCredit);

                    // Get the cumulative figure for the ENTIRE WEEK
                    Double weekToDateExpected = chosenWeek.
                            getWeeklyFigures(dayOfWeek).getFlexiExpected();

                    // Get the cumulative figure for the week up to
                    Double weekToDateRecorded = chosenWeek.
                            getWeeklyFigures(dayOfWeek).getFlexiRecorded();

                    double weekToDateBalance =
                            (weekToDateRecorded != null ? weekToDateRecorded : 0.0) -
                            (weekToDateExpected != null ? weekToDateExpected : 0.0);

                    day.setFlexiWeeklyCredit(weekToDateBalance);
                }

                ///////////////////////////////
                // The "sessional" time band //
                ///////////////////////////////

                // Are we expecting any time for the day?
                if (dailyFigures.getSessionExpected() != 0.0 ||
                        dailyFigures.getSessionRecorded() != 0.0) {

                    // Only display the sessional band if sessional
                    // time is being included in the report
                    if (sessional) {
                        reportMonth.setDisplaySessional(true);
                    }
                }

                ///////////////////////////////
                // The "sessional" time band //
                ///////////////////////////////

                // Are we expecting any time for the day?
                if (dailyFigures.getAdHocRecorded() != 0.0) {

                    // Only display the ad hoc band if sessional
                    // time is being included in the report
                    if (sessional) {
                        reportMonth.setDisplayAdHoc(true);
                    }
                }

                // Estimated figures?
                
                if (missingTimesheetEntry) {
                    day.getBase().setEstimated(99.99);
                }

                days.add(day);

                // Advance to the next day
                startCalendar.add(GregorianCalendar.DATE, 1);
            }

            // Allow space for each of the optional bands we'll be printing

            if (reportMonth.isDisplayFixed()) {
                requiredHeight += FIXED_HEIGHT;
            }

            if (reportMonth.isDisplaySemiFlexi()) {
                requiredHeight += SEMI_FLEXI_HEIGHT;
            }

            if (reportMonth.isDisplayFlexi()) {
                requiredHeight += FLEXI_HEIGHT;
            }

            if (reportMonth.isDisplaySessional()) {
                requiredHeight += SESSIONAL_HEIGHT;
            }

            if (reportMonth.isDisplayAdHoc()) {
                requiredHeight += AD_HOC_HEIGHT;
            }

            // Is there's insufficient space then force a new page
            if (requiredHeight > remainingHeight) {
                // Force a page break
                reportMonth.setPageBreak(true);
                remainingHeight = USABLE_PAGE_HEIGHT;
            } else {
                reportMonth.setPageBreak(false);
            }

            // Subtract the space we'll be using
            remainingHeight -= requiredHeight;

            // Is there space (in other words, do we need) a spacer?

            if (SPACER_HEIGHT < remainingHeight) {
                reportMonth.setTrailingSpace(true);
                remainingHeight -= SPACER_HEIGHT;
            } else {
                reportMonth.setTrailingSpace(false);
            }

            reportMonth.setDays(days);

            // Calculate the carried forward figures
            AllocationOfHours carriedForwardFigures =
                    (AllocationOfHours)broughForwardFigures.clone();
            carriedForwardFigures.add(totalFigures);

            // Put the brought forward figures in the report
            TimesheetDay broughtForward = new TimesheetDay();
            broughtForward.setBase(broughForwardFigures);
            broughtForward.setFixedShortfall(
                    broughForwardFigures.getFixedShortfall());
            broughtForward.setSemiFlexiOutstanding(
                    broughForwardFigures.getSemiFlexiShortfall());
            broughtForward.setFlexiCredit(
                    broughForwardFigures.getFlexiCredit());
            reportMonth.setBroughtForward(broughtForward);

            TimesheetDay totals = new TimesheetDay(totalFigures);
            totals.setFixedShortfall(totalFixedShortfall);
            totals.setSemiFlexiOutstanding(totalSemiFlexiOutstanding);
            totals.setFlexiCredit(totalFlexiCredit);
            reportMonth.setTotal(totals);

            // Put the carried forward figures in the report
            TimesheetDay carriedForward = new TimesheetDay();
            carriedForward.setBase(carriedForwardFigures);
            carriedForward.setFixedShortfall(
                    broughForwardFigures.getFixedShortfall());
            carriedForward.setSemiFlexiOutstanding(
                    broughForwardFigures.getSemiFlexiShortfall());
            carriedForward.setFlexiCredit(
                    broughForwardFigures.getFlexiCredit());
            reportMonth.setCarriedForward(carriedForward);

            if (reportMonth.isDisplayFixed() ||
                    reportMonth.isDisplaySemiFlexi() ||
                    reportMonth.isDisplayFlexi() ||
                    reportMonth.isDisplaySessional() ||
                    reportMonth.isDisplayAdHoc()) {
                monthReports.add(reportMonth);
            }
        }

        viewTimesheetReport(monthReports, parameters, COLLATION_REPORT);
    }

    /**
     * Executes the lowest level of our report generation... give it the data
     * and the class and let it get on with it.  Does very little other than
     * wrap the Jasper calls in an try... catch... block.
     *
     * @param data the report data
     * @param reportClass the report class
     */
    @SuppressWarnings("unchecked")
    private void viewTimesheetReport(ArrayList data, Map parameters, String reportClass) {

        if (parameters == null) {
            parameters = new ReportingUtilities().getBasicParameters();
        }

        try {
            JasperReport template = (JasperReport)JRLoader.loadObject(
                    getClass().getResource(reportClass));

            JasperPrint report = JasperFillManager.fillReport(
                    template, parameters, new JRBeanCollectionDataSource(data));

            JasperViewer.viewReport(report, false);

        } catch (JRException ex) {
            Logger.getLogger(CollationReport.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}